// Copyright 2016 The G3N Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// g3nshaders reads shaders files with ".glsl" extensions and generates
// a Go file containing strings with the content of these files.
// Also it builds maps associating include and shader names to its respective
// source strings.
// Usage:
// 		g3nshaders -in=<input_dir> -out<output_gofile> -v
// It is normally invoked by "go generate" inside the "shaders" directory
package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/format"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strings"
	"text/template"
)

// Program constants.
const (
	PROGNAME      = "g3nshaders"
	VMAJOR        = 0
	VMINOR        = 1
	SHADEREXT     = ".glsl"
	DIR_INCLUDE   = "include"
	TYPE_VERTEX   = "vertex"
	TYPE_FRAGMENT = "fragment"
	TYPE_GEOMETRY = "geometry"
)

//
// TEMPLATE is a Go template to generate the output file with the shaders' sources and
// maps describing the include and shader names and programs shaders.
//
const TEMPLATE = `// Code generated by G3NSHADERS. DO NOT EDIT.
// To regenerate this file install 'g3nshaders' and execute:
// 'go generate' in this folder.

package {{.Pkg}}

{{range .Includes}}
const include_{{.Name}}_source = ` + "`{{.Source}}`" + `
{{end}}

{{range .Shaders}}
const {{.Name}}_source = ` + "`{{.Source}}`" + `
{{end}}

// Maps include name with its source code
var includeMap = map[string]string {
{{range .Includes}}
	"{{- .Name}}": include_{{.Name}}_source, {{end}}
}

// Maps shader name with its source code
var shaderMap = map[string]string {
{{range .Shaders}}
	"{{- .Name}}": {{.Name}}_source, {{end}}
}

// Maps program name with Proginfo struct with shaders names
var programMap = map[string]ProgramInfo{
{{ range $progName, $progInfo := .Programs }}
	"{{$progName}}": { "{{$progInfo.Vertex}}","{{$progInfo.Fragment}}","{{$progInfo.Geometry}}" }, {{end}}
}
`

// Command line options
var (
	oVersion = flag.Bool("version", false, "Show version and exits")
	oInp     = flag.String("in", ".", "Input directory")
	oOut     = flag.String("out", "sources.go", "Go output file")
	oPackage = flag.String("pkg", "shaders", "Package name")
	oVerbose = flag.Bool("v", false, "Show files being processed")
)

// Valid shader types
var shaderTypes = map[string]bool{
	TYPE_VERTEX:   true,
	TYPE_FRAGMENT: true,
	TYPE_GEOMETRY: true,
}

// fileInfo describes a shader or include file name and source code
type fileInfo struct {
	Name    string // shader or include name
	Source  string // shader or include source code
	Include bool   // true if include, false otherwise
}

// progInfo describes all the shader names of an specific program
// If the program doesn't use the geometry shader it is set as an empty string
type progInfo struct {
	Vertex   string // vertex shader name
	Fragment string // fragment shader name
	Geometry string // geometry shader name
}

// templInfo contains all information needed for the template expansion
type templInfo struct {
	Count    int                 // number of shader files processed
	Pkg      string              // name of the package for the generated output file
	Includes []fileInfo          // list of include files found
	Shaders  []fileInfo          // list of shader files found
	Programs map[string]progInfo // map of shader programs found
}

var templData templInfo // global template data
var logger *log.Logger  // global logger

func main() {

	// Parse command line parameters
	flag.Usage = usage
	flag.Parse()

	// If requested, print version and exits
	if *oVersion {
		fmt.Fprintf(os.Stderr, "%s v%d.%d\n", PROGNAME, VMAJOR, VMINOR)
		return
	}

	// Creates logger
	logger = log.New(os.Stdout, "G3NSHADERS ", log.Ldate|log.Ltime)

	// Initialize template data
	templData.Pkg = *oPackage
	templData.Programs = make(map[string]progInfo)

	// Process the current directory and its subdirectories recursively
	// appending information into templData
	processDir(*oInp, false)
	if templData.Count == 0 {
		log.Print("No shader files found")
		return
	}

	// Generates output file from TEMPLATE
	generate(*oOut)
}

// processDir processes recursively all shaders files in the specified directory
func processDir(dir string, include bool) {

	// Open directory to process
	f, err := os.Open(dir)
	if err != nil {
		panic(err)
	}
	defer f.Close()

	// Read all file entries from the directory
	finfos, err := f.Readdir(0)
	if err != nil {
		panic(err)
	}

	// Process all directory entries.
	for _, fi := range finfos {
		if fi.IsDir() {
			dirInclude := include
			if fi.Name() == DIR_INCLUDE {
				dirInclude = true
			}
			processDir(filepath.Join(dir, fi.Name()), dirInclude)
		} else {
			processFile(filepath.Join(dir, fi.Name()), include)
		}
	}
}

// processFile process one file checking if it has the shaders extension,
// otherwise it is ignored.
// If the include flag is true the file is an include file otherwise it
// it a shader
func processFile(file string, include bool) {

	// Ignore file if it has not the shader extension
	fext := filepath.Ext(file)
	if fext != SHADEREXT {
		return
	}

	if *oVerbose {
		logger.Printf("Processing: %s", file)
	}

	// Get the file base name and its name with the extension
	fbase := filepath.Base(file)
	fname := fbase[:len(fbase)-len(fext)]

	// If not in include directory, the file must be a shader program
	// which name must have the format: <name>_<shader_type>
	if !include {
		parts := strings.Split(string(fname), "_")
		if len(parts) < 2 {
			logger.Print("   IGNORED: file does not have a valid shader name")
			return
		}
		stype := parts[len(parts)-1]
		if !shaderTypes[stype] {
			logger.Print("   IGNORED: invalid shader type")
			return
		}
		sname := strings.Join(parts[:len(parts)-1], "_")
		pinfo, ok := templData.Programs[sname]
		if !ok {
			templData.Programs[sname] = pinfo
		}
		switch stype {
		case TYPE_VERTEX:
			pinfo.Vertex = fname
		case TYPE_FRAGMENT:
			pinfo.Fragment = fname
		case TYPE_GEOMETRY:
			pinfo.Geometry = fname
		}
		templData.Programs[sname] = pinfo
	}

	// Reads all file data
	f, err := os.Open(file)
	if err != nil {
		panic(err)
	}
	defer f.Close()
	data, err := ioutil.ReadAll(f)
	if err != nil {
		panic(err)
	}

	// Appends entry in Includes or Shaders
	if include {
		templData.Includes = append(templData.Includes, fileInfo{
			Name:   fname,
			Source: string(data),
		})
	} else {
		templData.Shaders = append(templData.Shaders, fileInfo{
			Name:   fname,
			Source: string(data),
		})
	}
	templData.Count++
}

// generate generates output go file with shaders sources from TEMPLATE
func generate(file string) {

	if *oVerbose {
		logger.Printf("Generating: %s", file)
	}

	// Parses the template
	tmpl := template.New("tmpl")
	tmpl, err := tmpl.Parse(TEMPLATE)
	if err != nil {
		panic(err)
	}

	// Expands template to buffer
	var buf bytes.Buffer
	err = tmpl.Execute(&buf, &templData)
	if err != nil {
		panic(err)
	}

	// Formats buffer as Go source
	p, err := format.Source(buf.Bytes())
	if err != nil {
		panic(err)
	}

	// Writes formatted source to output file
	f, err := os.Create(file)
	if err != nil {
		panic(err)
	}
	f.Write(p)
	f.Close()
}

// usage shows the application usage
func usage() {

	fmt.Fprintf(os.Stderr, "%s v%d.%d\n", PROGNAME, VMAJOR, VMINOR)
	fmt.Fprintf(os.Stderr, "usage: %s [options]\n", strings.ToLower(PROGNAME))
	flag.PrintDefaults()
	os.Exit(2)
}
